package ar.uba.fi.tonyvaliente.utils.io;

import java.io.IOException;
import java.io.InputStream;
import java.util.BitSet;


/**
 * Representa un stream de entrada de bits que pueden
 * leerse de cualquier <code>java.io.InputStream</code>.
 * Dado que los <code>InputStream</code>s operan unicamente
 * con bytes, los objetos de la presente clase requieren
 * que se indique la forma en que esta relleno el ultimo byte,
 * la cual esta dada por el enumerado <code>ByteCompletionMode</code>.
 * @author dmorello
 */
public class InputBitstream {

	private InputStream stream;
	private ByteCompletionMode completionMode;
	private byte[] lastBytes;
	private byte bitsInLastByte;
	private int validBytes;
	private int mask;
	
	/**
	 * Crea una instancia de la clase asociada a un <code>InputStream</code>
	 * con una forma de relleno del ultimo byte dada por un
	 * <code>ByteCompletionMode</code>. 
	 * @param stream <code>InputStream</code> del cual leer los bits
	 * @param completionMode modo de relleno del ultimo byte de <code>stream</code>
	 */
	public InputBitstream(InputStream stream, ByteCompletionMode completionMode) {
		this.stream = stream;
		this.completionMode = completionMode;
	}
	
	/**
	 * Lee un bit del bitstream.
	 * 
	 * Para determinar el fin del bitstream, luego de
	 * leer el ultimo byte del <code>InputStream</code> asociado, determina
	 * cuantos bits de ese byte forman parte del bitstream de acuerdo al
	 * <code>ByteCompletionMode</code> recibido por parametro en el constructor.
	 * 
	 * Si dicho parametro era <code>ByteCompletionMode.NONE</code>, considera que
	 * todos los bits de ese ultimo byte forman parte del bitstream.
	 *  
	 * @return objeto Boolean con el bit leido, o <code>null</code> si se
	 * llego al fin del bitstream y no fue posible leer un bit.
	 * @throws IOException si ocurre algun error al leer
	 */
	public Boolean read() throws IOException {
		if (lastBytes == null) {
			this.lastBytes = new byte[2];
			this.validBytes = stream.read(lastBytes);
			this.bitsInLastByte = 8;
			this.mask = 0x80;
		}
		
		Boolean bit = null;
		
		if ((validBytes == 2 && bitsInLastByte == 0)) {
			lastBytes[0] = lastBytes[1];
			bitsInLastByte = 8;
			mask = 0x80;
			int c = stream.read();
			if (c != -1) {
				lastBytes[1] = (byte)c;
			} else {
				// fin de archivo
				validBytes = 1;
			}
		}
		
		if (validBytes == 1 && bitsInLastByte == 8) {
			byte byteToApplyMask = lastBytes[0];
			switch (completionMode) {
			case ZERO_AND_TRAILING_ONES:
				byteToApplyMask = (byte)~byteToApplyMask;
			case ONE_AND_TRAILING_ZEROS:
				byte zeroMask = (byte)1;
				boolean zero; 
				do {
					zero = ((byteToApplyMask & zeroMask) == 0);
					bitsInLastByte--;
					zeroMask <<= 1;
				} while (zero);
				break;
			}
		}
		
		if (bitsInLastByte > 0) {
			bit = Boolean.valueOf((lastBytes[0] & mask) != 0);
			mask >>= 1;
			bitsInLastByte--;
		}
		
		return bit;
	}
	
	/**
	 * Lee varios bits del bitstream y los escribe en sucesivas posiciones de
	 * un array de <code>boolean</code>. Lo hace segun los criterios utilizados en
	 * el metodo <code>read()</code>
	 * @param bits array de <code>boolean</code> donde colocar los bits leidos
	 * @return cantidad de bits leidos exitosamente.
	 * @throws IOException si ocurre algun error al leer
	 */
	public int read(boolean[] bits) throws IOException {
		int count = 0;
		Boolean bit = null;
		for (int i = 0; i < bits.length; i++) {
			bit = read();
			if (bit != null) {
				bits[i] = bit.booleanValue();
				count++;
			} else {
				break;
			}
		}
		return count;
	}
	
	/**
	 * Lee varios bits del bitstream y los escribe en sucesivas posiciones de
	 * un <code>BitSet</code>. Lo hace segun los criterios utilizados en
	 * el metodo <code>read()</code>
	 * @param bitSet <code>BitSet</code> donde colocar los bits leidos
	 * @return cantidad de bits leidos exitosamente.
	 * @throws IOException si ocurre algun error al leer
	 */
	public int read(BitSet bitSet) throws IOException {
		int count = 0;
		Boolean bit = null;
		for (int i = 0; i < bitSet.size(); i++) {
			bit = read();
			if (bit != null) {
				bitSet.set(i, bit.booleanValue());
				count++;
			} else {
				break;
			}
		}
		return count;
	}
	
	/**
	 * Cierra el bitstream y lo deja inhabilitado para nuevas lecturas.
	 * @throws IOException si ocurre un error al cerrar
	 */
	public void close() throws IOException {
		stream.close();
		completionMode = null;
	}
}
